// Soulful Evolution CMS - Production Level JavaScript // Author: Pavan // Version: 1.0.0 class SoulfulCMS { constructor() { this.isEditMode = false; this.currentData = []; this.originalData = []; this.apiUrl = window.location.origin.includes('localhost') ? 'http://localhost:3001/api' : '/api'; this.init(); } init() { this.loadData(); this.setupEventListeners(); } // Data Management async loadData() { try { this.showNotification('Loading content...', 'info'); const response = await fetch(`${this.apiUrl}/content`); if (!response.ok) { throw new Error('Failed to load content'); } this.currentData = await response.json(); this.originalData = JSON.parse(JSON.stringify(this.currentData)); this.renderContent(); this.updateStats(); } catch (error) { console.error('Error loading data:', error); this.showNotification('Failed to load content. Using offline data.', 'error'); this.loadOfflineData(); } } loadOfflineData() { // Fallback to static data if API fails this.originalData = [ { id: 1, title: "Root Chakra Meditation: Grounding Your Energy for Inner Strength", series: "Chakra Meditations", status: "uploaded", priority: "high", youtube_title: "Root Chakra Meditation: Grounding Your Energy for Inner Strength", youtube_description: "Discover the power of your root chakra with this guided meditation designed to help you feel more grounded, secure, and connected to the earth. In this transformative session, you'll learn how to activate and balance your root chakra, the foundation of your entire chakra system.\n\n🌟 What you'll experience:\n• Deep grounding techniques\n• Energy healing visualization\n• Connection to earth's stabilizing force\n• Inner strength activation\n\n✨ Perfect for: Beginners and experienced practitioners seeking stability and grounding.\n\n🔮 Book a personal reading: https://soulfulevolution.com\n\n#RootChakra #Meditation #Grounding #ChakraHealing #Spirituality\n\n#TAGS#\nroot chakra, meditation, grounding, healing, spirituality", thumbnail_text: "ROOT CHAKRA HEALING", script_status: "complete", speakflow_link: "https://speakflow.com/root-chakra-script", drive_link: "https://drive.google.com/folder/root-chakra-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/root-chakra-artwork", upload_date: "2024-12-15", assigned_to: ["A", "S"], notes: "First in chakra series. Great engagement! High retention rate.", thumbnail_file: null, script_file: null }, { id: 2, title: "Sacral Chakra Healing: Unlock Your Creative Power and Emotional Flow", series: "Chakra Meditations", status: "uploaded", priority: "high", youtube_title: "Sacral Chakra Healing: Unlock Your Creative Power and Emotional Flow", youtube_description: "Awaken your sacral chakra and unleash your creative potential with this powerful guided meditation. Located just below your navel, the sacral chakra governs creativity, sexuality, and emotional well-being.\n\n🧡 Benefits of this meditation:\n• Enhanced creativity and inspiration\n• Improved emotional balance\n• Greater self-expression\n• Renewed passion for life\n• Healing past emotional wounds\n\n🎨 Perfect for: Artists, creators, and anyone seeking emotional healing and creative inspiration.\n\n💫 Connect with us: https://soulfulevolution.com\n\n#SacralChakra #Creativity #EmotionalHealing #ChakraBalancing #Meditation", thumbnail_text: "SACRAL CHAKRA CREATIVITY", script_status: "complete", speakflow_link: "https://speakflow.com/sacral-chakra-script", drive_link: "https://drive.google.com/folder/sacral-chakra-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/sacral-chakra-artwork", upload_date: "2024-12-17", assigned_to: ["A", "S"], notes: "Part 2 of series. Focus on creativity themes.", thumbnail_file: null, script_file: null }, { id: 3, title: "Solar Plexus Chakra Meditation: Build Confidence and Personal Power", series: "Chakra Meditations", status: "recording", priority: "urgent", youtube_title: "Solar Plexus Chakra Meditation: Build Confidence and Personal Power", youtube_description: "Transform your self-confidence and personal power with this solar plexus chakra meditation. This energy center, located in your upper abdomen, is your source of personal power, confidence, and self-worth.\n\n💛 This meditation will help you:\n• Build unshakeable confidence\n• Strengthen your personal boundaries\n• Overcome self-doubt and fear\n• Activate your inner warrior\n• Manifest your goals with clarity\n\n🔥 Ideal for: Anyone struggling with confidence, self-worth, or personal power issues.\n\n⭐ Book a session: https://soulfulevolution.com\n\n#SolarPlexusChakra #Confidence #PersonalPower #SelfWorth #Meditation", thumbnail_text: "SOLAR PLEXUS POWER", script_status: "complete", speakflow_link: "https://speakflow.com/solar-plexus-script", drive_link: "https://drive.google.com/folder/solar-plexus-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/solar-plexus-artwork", upload_date: null, assigned_to: ["T"], notes: "Recording in progress. Tim handling this personally.", thumbnail_file: null, script_file: null }, { id: 4, title: "Heart Chakra Opening: Heal Your Heart and Attract Love", series: "Chakra Meditations", status: "pending", priority: "medium", youtube_title: "Heart Chakra Opening: Heal Your Heart and Attract Love", youtube_description: "Open your heart chakra and invite more love into your life with this gentle yet powerful meditation. The heart chakra is your center of love, compassion, and connection.\n\n💚 Experience:\n• Deep emotional healing\n• Increased capacity for love\n• Better relationships\n• Self-compassion and forgiveness\n• Connection to universal love\n\n❤️ Perfect for: Those healing from heartbreak, wanting to attract love, or seeking deeper compassion.\n\n🌸 Learn more: https://soulfulevolution.com\n\n#HeartChakra #Love #Healing #Compassion #Meditation", thumbnail_text: "HEART CHAKRA LOVE", script_status: "draft", speakflow_link: "", drive_link: "", gpt_mock_link: "", upload_date: null, assigned_to: ["S"], notes: "Needs script review before production.", thumbnail_file: null, script_file: null, youtube_views: 0, youtube_likes: 0, youtube_comments: 0, view_count: 0, like_count: 0, comment_count: 0 }, { id: 5, title: "Throat Chakra Activation: Speak Your Truth with Confidence", series: "Chakra Meditations", status: "script", priority: "high", youtube_title: "Throat Chakra Activation: Speak Your Truth with Confidence", youtube_description: "Unlock your authentic voice and express your truth with this powerful throat chakra meditation. The throat chakra governs communication, self-expression, and speaking your personal truth.\n\n💙 This meditation helps you:\n• Clear communication blockages\n• Build confidence in self-expression\n• Release fear of judgment\n• Strengthen your authentic voice\n• Improve public speaking abilities\n\n🗣️ Perfect for: Anyone struggling with communication, public speaking, or expressing their truth.\n\n🎤 Connect: https://soulfulevolution.com\n\n#ThroatChakra #Communication #SelfExpression #Confidence #Meditation", thumbnail_text: "THROAT CHAKRA VOICE", script_status: "review", speakflow_link: "https://speakflow.com/throat-chakra-script", drive_link: "https://drive.google.com/folder/throat-chakra-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/throat-chakra-artwork", upload_date: null, assigned_to: ["A", "G"], notes: "Script needs review for authenticity themes. Grace working on blue-themed artwork.", thumbnail_file: null, script_file: null, youtube_views: 0, youtube_likes: 0, youtube_comments: 0, view_count: 0, like_count: 0, comment_count: 0 }, { id: 6, title: "Angel Number 111: Manifestation and New Beginnings", series: "Angel Messages", status: "editing", priority: "urgent", youtube_title: "Angel Number 111: Your Angels Are Calling - Manifestation Portal Open!", youtube_description: "Discover the powerful meaning behind Angel Number 111 and how your angels are guiding you toward manifestation and new beginnings. This isn't just coincidence - it's divine communication!\n\n✨ What 111 means:\n• Manifestation portal is open\n• New beginnings are coming\n• Your thoughts are rapidly manifesting\n• Angels are supporting your journey\n• Time to focus on positive thinking\n\n👼 Perfect for: Those seeing 111 repeatedly and seeking spiritual guidance.\n\n📞 Book an angel reading: https://soulfulevolution.com\n\n#AngelNumbers #111 #Manifestation #Angels #Spirituality #NewBeginnings", thumbnail_text: "ANGEL NUMBER 111", script_status: "complete", speakflow_link: "https://speakflow.com/angel-111-script", drive_link: "https://drive.google.com/folder/angel-111-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/angel-111-artwork", upload_date: null, assigned_to: ["S", "T"], notes: "Urgent for trending topic. Sasha in final edit phase, Tim reviewing for quality.", thumbnail_file: null, script_file: null, youtube_views: 0, youtube_likes: 0, youtube_comments: 0, view_count: 0, like_count: 0, comment_count: 0 }, { id: 7, title: "Past Life Regression: Discover Your Soul's Journey", series: "Psychic Development", status: "pending", priority: "medium", youtube_title: "Past Life Regression Meditation: Uncover Your Soul's Ancient Wisdom", youtube_description: "Journey through time and explore your past lives with this guided regression meditation. Discover the karmic lessons and soul connections that shape your current life path.\n\n🔮 Experience:\n• Safe guided past life exploration\n• Understanding of karmic patterns\n• Soul purpose clarity\n• Release of past life trauma\n• Connection to ancient wisdom\n\n🌟 Perfect for: Spiritually curious individuals ready for deep soul work.\n\n📚 Learn more: https://soulfulevolution.com\n\n#PastLife #Regression #Karma #SoulJourney #Meditation #Spirituality", thumbnail_text: "PAST LIFE JOURNEY", script_status: "draft", speakflow_link: "", drive_link: "", gpt_mock_link: "", upload_date: null, assigned_to: ["T"], notes: "Tim wants to handle this personally. Sensitive topic requiring careful approach.", thumbnail_file: null, script_file: null, youtube_views: 0, youtube_likes: 0, youtube_comments: 0, view_count: 0, like_count: 0, comment_count: 0 }, { id: 8, title: "Third Eye Chakra Opening: Awakening Your Intuition", series: "Chakra Meditations", status: "uploaded", priority: "high", youtube_title: "Third Eye Chakra Meditation: Open Your Psychic Abilities & Intuition", youtube_description: "Awaken your third eye chakra and unlock your natural psychic abilities with this transformative meditation. Located between your eyebrows, this energy center governs intuition, psychic sight, and spiritual insight.\n\n💜 Benefits include:\n• Enhanced intuitive abilities\n• Clearer psychic visions\n• Better dream recall\n• Spiritual insight and wisdom\n• Connection to higher consciousness\n\n🔮 Perfect for: Those ready to develop their psychic gifts and spiritual sight.\n\n✨ Psychic readings: https://soulfulevolution.com\n\n#ThirdEye #PsychicAbilities #Intuition #ChakraHealing #Meditation #Spirituality", thumbnail_text: "THIRD EYE AWAKENING", script_status: "complete", speakflow_link: "https://speakflow.com/third-eye-script", drive_link: "https://drive.google.com/folder/third-eye-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/third-eye-artwork", upload_date: "2024-12-20", assigned_to: ["A", "S"], notes: "Performing really well! Lots of comments about people having visions during meditation.", thumbnail_file: null, script_file: null, youtube_views: 31250, youtube_likes: 1450, youtube_comments: 127, view_count: 31250, like_count: 1450, comment_count: 127 }, { id: 9, title: "Full Moon Manifestation Ritual: Amplify Your Desires", series: "Spiritual Guidance", status: "recording", priority: "urgent", youtube_title: "Full Moon Manifestation Ritual - Manifest Your Dreams Tonight!", youtube_description: "Harness the powerful energy of the full moon to amplify your manifestation practice. This complete ritual guide will help you align with lunar energy for maximum manifestation power.\n\n🌕 What you'll learn:\n• Complete full moon ritual steps\n• Moon water creation process\n• Manifestation journal techniques\n• Crystal charging methods\n• Release and attract practices\n\n🌙 Perfect for: Moon magic practitioners and manifestation enthusiasts.\n\n🔮 Moon readings: https://soulfulevolution.com\n\n#FullMoon #Manifestation #MoonRitual #LunarMagic #Spirituality #MoonMagic", thumbnail_text: "FULL MOON MAGIC", script_status: "complete", speakflow_link: "https://speakflow.com/full-moon-script", drive_link: "https://drive.google.com/folder/full-moon-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/full-moon-artwork", upload_date: null, assigned_to: ["T", "P"], notes: "Time-sensitive for upcoming full moon! Tim recording, Pavan handling tech setup.", thumbnail_file: null, script_file: null, youtube_views: 0, youtube_likes: 0, youtube_comments: 0, view_count: 0, like_count: 0, comment_count: 0 }, { id: 10, title: "Connecting with Your Spirit Guides: Divine Communication", series: "Psychic Development", status: "uploaded", priority: "medium", youtube_title: "How to Connect with Your Spirit Guides - Direct Divine Communication", youtube_description: "Learn how to establish clear communication with your spirit guides and receive divine guidance in your daily life. Your guides are always with you - it's time to learn how to hear them!\n\n🙏 This meditation teaches:\n• How to identify your spirit guides\n• Techniques for clear communication\n• Signs and symbols from guides\n• Strengthening your spiritual connection\n• Daily practices for ongoing guidance\n\n👼 Perfect for: Anyone seeking divine guidance and spiritual mentorship.\n\n📱 Spirit guide readings: https://soulfulevolution.com\n\n#SpiritGuides #DivineGuidance #Spirituality #Meditation #PsychicDevelopment", thumbnail_text: "SPIRIT GUIDE CONNECTION", script_status: "complete", speakflow_link: "https://speakflow.com/spirit-guides-script", drive_link: "https://drive.google.com/folder/spirit-guides-footage", gpt_mock_link: "https://chatgpt.com/gpt-mock/spirit-guides-artwork", upload_date: "2024-12-12", assigned_to: ["A", "G", "S"], notes: "Amazing response! People sharing incredible guide connection stories in comments.", thumbnail_file: null, script_file: null, youtube_views: 28940, youtube_likes: 1320, youtube_comments: 203, view_count: 28940, like_count: 1320, comment_count: 203 }, { id: 11, title: "Twin Flame vs Soulmate: Understanding Divine Connections", series: "Spiritual Guidance", status: "pending", priority: "low", youtube_title: "Twin Flame vs Soulmate: Which Divine Connection Do You Have?", youtube_description: "Confused about twin flames and soulmates? Learn the key differences between these divine connections and discover which type of relationship you're experiencing.\n\n💕 Topics covered:\n• Twin flame characteristics and signs\n• Soulmate connections explained\n• The twin flame journey stages\n• How to recognize your divine counterpart\n• Healing and preparation work\n\n❤️ Perfect for: Those experiencing intense spiritual connections in relationships.\n\n💖 Love readings: https://soulfulevolution.com\n\n#TwinFlame #Soulmate #DivineConnection #Love #Spirituality #Relationships", thumbnail_text: "TWIN FLAME SOULMATE", script_status: "pending", speakflow_link: "", drive_link: "", gpt_mock_link: "", upload_date: null, assigned_to: [], notes: "Popular requested topic. Needs careful handling due to sensitive nature of twin flame journey.", thumbnail_file: null, script_file: null, youtube_views: 0, youtube_likes: 0, youtube_comments: 0, view_count: 0, like_count: 0, comment_count: 0 }, { id: 12, title: "Crown Chakra Meditation: Divine Connection and Enlightenment", series: "Chakra Meditations", status: "script", priority: "high", youtube_title: "Crown Chakra Meditation: Connect to Divine Consciousness & Higher Self", youtube_description: "Experience divine connection and spiritual enlightenment through this powerful crown chakra meditation. The crown chakra is your gateway to higher consciousness and universal wisdom.\n\n🔮 This meditation brings:\n• Connection to divine consciousness\n• Spiritual enlightenment experiences\n• Access to universal knowledge\n• Higher self alignment\n• Transcendence of ego limitations\n\n✨ Perfect for: Advanced practitioners ready for deep spiritual experiences.\n\n🙏 Spiritual guidance: https://soulfulevolution.com\n\n#CrownChakra #Enlightenment #Divine #Consciousness #Meditation #Spirituality", thumbnail_text: "CROWN CHAKRA DIVINE", script_status: "draft", speakflow_link: "https://speakflow.com/crown-chakra-script", drive_link: "", gpt_mock_link: "https://chatgpt.com/gpt-mock/crown-chakra-artwork", upload_date: null, assigned_to: ["A", "T"], notes: "Final chakra in series. Allison working on script, Tim reviewing for spiritual accuracy.", thumbnail_file: null, script_file: null, youtube_views: 0, youtube_likes: 0, youtube_comments: 0, view_count: 0, like_count: 0, comment_count: 0 } ]; // Clone data for editing this.currentData = JSON.parse(JSON.stringify(this.originalData)); this.renderContent(); this.updateStats(); } async saveData() { try { this.showNotification('Saving changes...', 'info'); // Save all modified items const savePromises = this.currentData.map(async (item) => { const original = this.originalData.find(o => o.id === item.id); if (!original || JSON.stringify(item) !== JSON.stringify(original)) { // Item was modified, save it return this.saveItem(item); } }); await Promise.all(savePromises.filter(Boolean)); // Update original data to match current this.originalData = JSON.parse(JSON.stringify(this.currentData)); this.showNotification('All changes saved successfully!', 'success'); } catch (error) { console.error('Error saving data:', error); this.showNotification('Failed to save some changes', 'error'); } } async saveItem(item) { const response = await fetch(`${this.apiUrl}/content/${item.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item) }); if (!response.ok) { throw new Error(`Failed to save ${item.title}`); } return response.json(); } // Event Listeners setupEventListeners() { // Edit mode toggle document.getElementById('editModeToggle').addEventListener('click', () => this.toggleEditMode()); // Action buttons document.getElementById('addContentBtn').addEventListener('click', () => this.showAddContentForm()); document.getElementById('generateReportBtn').addEventListener('click', () => this.generateReport()); // Filter functionality document.querySelectorAll('.filter-select').forEach(select => { select.addEventListener('change', (e) => this.filterContent(e.target.value)); }); // Save shortcut document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 's') { e.preventDefault(); this.saveData(); } }); // Setup View Description links (work always, not just in edit mode) this.setupDescriptionLinks(); } // Edit Mode Management toggleEditMode() { const container = document.querySelector('.container'); const button = document.getElementById('editModeToggle'); this.isEditMode = !this.isEditMode; if (this.isEditMode) { container.classList.add('edit-mode'); button.textContent = '💾 Save & Exit'; button.style.background = '#e53e3e'; this.renderContent(); // Re-render to add delete column this.enableEditMode(); this.showNotification('Edit mode enabled - Click any field to edit', 'info'); } else { this.saveData(); container.classList.remove('edit-mode'); button.textContent = '📝 Enable Edit Mode'; button.style.background = '#805ad5'; this.renderContent(); // Re-render to remove delete column this.showNotification('Changes saved and edit mode disabled', 'success'); } } enableEditMode() { // Make all editable fields clickable this.setupEditableFields(); } setupDescriptionLinks() { // This runs always, not just in edit mode document.addEventListener('click', (e) => { if (e.target.matches('[data-field="youtube_description"]')) { e.preventDefault(); const itemId = parseInt(e.target.closest('tr').getAttribute('data-id')); const item = this.currentData.find(d => d.id === itemId); if (this.isEditMode) { // In edit mode, allow editing this.editField(e.target, 'youtube_description', 'textarea'); } else { // Not in edit mode, just view this.showDescriptionModal(item.title, item.youtube_description); } } }); } showDescriptionModal(title, descriptionContent) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; // Parse description and tags const { description, tags } = this.parseDescriptionAndTags(descriptionContent); // Format the description nicely const formattedDescription = this.formatYouTubeDescription(description || 'No description available'); // Render tags if they exist const tagsSection = tags.length > 0 ? `

🏷️ Tags

${this.renderTags(tags)}
` : ''; modal.innerHTML = `

${title}

📝 YouTube Description

${formattedDescription}
${tagsSection}
`; document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); } formatYouTubeDescription(description) { if (!description || description === 'No description available') { return '
No description available
'; } // Replace line breaks with proper spacing let formatted = description.replace(/\n\n/g, '

'); formatted = formatted.replace(/\n/g, '
'); // Wrap in paragraphs formatted = '

' + formatted + '

'; // Style emojis and bullet points formatted = formatted.replace(/^•\s*/gm, ' '); // Style hashtags formatted = formatted.replace(/#(\w+)/g, '#$1'); // Style URLs (make them clickable) formatted = formatted.replace(/(https?:\/\/[^\s]+)/g, '$1'); // Style section headers (text ending with colon) formatted = formatted.replace(/^([^<\n]*:)$/gm, '
$1
'); return formatted; } setupEditableFields() { // Video titles document.querySelectorAll('[data-field="title"]').forEach(el => { el.classList.add('editable'); el.addEventListener('click', () => this.editField(el, 'title', 'text')); }); // Status fields document.querySelectorAll('[data-field="status"]').forEach(el => { el.classList.add('editable'); el.addEventListener('click', () => this.editField(el, 'status', 'select')); }); // Priority fields document.querySelectorAll('[data-field="priority"]').forEach(el => { el.classList.add('editable'); el.addEventListener('click', () => this.editField(el, 'priority', 'select')); }); // Links document.querySelectorAll('[data-field="speakflow_link"], [data-field="drive_link"], [data-field="gpt_mock_link"]').forEach(el => { el.classList.add('editable'); el.addEventListener('click', (e) => { e.preventDefault(); const field = el.getAttribute('data-field'); this.editField(el, field, 'url'); }); }); // Notes document.querySelectorAll('[data-field="notes"]').forEach(el => { el.classList.add('editable'); el.addEventListener('click', () => this.editField(el, 'notes', 'textarea')); }); // Series document.querySelectorAll('[data-field="series"]').forEach(el => { el.classList.add('editable'); el.addEventListener('click', () => this.editField(el, 'series', 'select')); }); // Team Assignment document.querySelectorAll('[data-field="assigned_to"]').forEach(el => { el.classList.add('editable'); el.addEventListener('click', () => this.editField(el, 'assigned_to', 'checkboxes')); }); // Script Status document.querySelectorAll('.script-status').forEach(el => { el.classList.add('editable'); el.addEventListener('click', () => this.editField(el, 'script_status', 'select')); }); // Thumbnails document.querySelectorAll('[data-field="thumbnails"]').forEach(el => { el.classList.add('editable'); const contentId = el.getAttribute('data-content-id'); el.addEventListener('click', () => { if (this.isEditMode) { this.showThumbnailManager(contentId); } else { this.showThumbnailViewer(contentId); } }); }); } // Field Editing editField(element, fieldName, inputType) { if (!this.isEditMode) return; const itemId = parseInt(element.closest('tr').getAttribute('data-id')); const item = this.currentData.find(d => d.id === itemId); const currentValue = item[fieldName] || ''; this.showEditModal(fieldName, currentValue, inputType, (newValue) => { item[fieldName] = newValue; this.updateDisplayValue(element, fieldName, newValue); }); } showEditModal(fieldName, currentValue, inputType, onSave) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; const fieldLabel = this.getFieldLabel(fieldName); let inputHTML = ''; switch (inputType) { case 'textarea': if (fieldName === 'youtube_description') { // Special handling for combined description and tags const { description, tags } = this.parseDescriptionAndTags(currentValue); const tagsString = tags.join(', '); inputHTML = `

Tags will be automatically added to the description.

`; } else { inputHTML = ``; } break; case 'select': inputHTML = this.getSelectHTML(fieldName, currentValue); break; case 'url': inputHTML = ``; break; case 'checkboxes': inputHTML = this.getCheckboxesHTML(fieldName, currentValue); break; case 'tags': inputHTML = this.getTagsInputHTML(currentValue); break; default: inputHTML = ``; } modal.innerHTML = `

Edit ${fieldLabel}

${inputHTML}
`; document.body.appendChild(modal); // Focus and setup save const saveBtn = document.getElementById('saveBtn'); if (inputType === 'checkboxes') { const save = () => { const checkboxes = modal.querySelectorAll('input[type="checkbox"]:checked'); const newValue = Array.from(checkboxes).map(cb => cb.value); onSave(newValue); modal.remove(); }; saveBtn.addEventListener('click', save); // Handle create new team member button const createBtn = modal.querySelector('#createNewTeamMember'); if (createBtn) { createBtn.addEventListener('click', () => this.showCreateTeamMemberForm(modal, save)); } } else { // Handle special case for youtube_description with separate inputs if (fieldName === 'youtube_description' && inputType === 'textarea') { const descInput = document.getElementById('descriptionInput'); const tagsInput = document.getElementById('tagsInput'); descInput.focus(); const save = () => { const description = descInput.value.trim(); const tagsString = tagsInput.value.trim(); const tags = tagsString ? tagsString.split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag.length > 0) : []; const newValue = this.combineDescriptionAndTags(description, tags); if (newValue !== currentValue) { onSave(newValue); } modal.remove(); }; saveBtn.addEventListener('click', save); } else { const input = document.getElementById('editInput'); input.focus(); if (input.type === 'text') input.select(); const save = async () => { let newValue = input.value.trim(); // Handle special input types if (inputType === 'tags') { newValue = newValue.split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag.length > 0); } else if (inputType === 'select' && fieldName === 'series' && newValue === '__CREATE_NEW__') { // Handle create new series const seriesName = prompt('Enter new series name:'); if (seriesName && seriesName.trim()) { newValue = seriesName.trim(); this.showNotification(`New series "${newValue}" created!`, 'success'); } else { return; // User cancelled or entered empty name } } if (JSON.stringify(newValue) !== JSON.stringify(currentValue)) { onSave(newValue); } modal.remove(); }; saveBtn.addEventListener('click', save); // Add keyboard shortcut for non-textarea inputs if (input && input.type !== 'textarea') { input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { save(); } }); } } } modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); }); } getSelectHTML(fieldName, currentValue) { const options = { status: [ { value: 'pending', label: 'Pending' }, { value: 'script', label: 'Script' }, { value: 'recording', label: 'Recording' }, { value: 'editing', label: 'Editing' }, { value: 'uploaded', label: 'Uploaded' } ], priority: [ { value: 'urgent', label: 'Urgent' }, { value: 'high', label: 'High' }, { value: 'medium', label: 'Medium' }, { value: 'low', label: 'Low' } ], series: [ { value: 'Chakra Meditations', label: 'Chakra Meditations' }, { value: 'Spiritual Guidance', label: 'Spiritual Guidance' }, { value: 'Psychic Development', label: 'Psychic Development' }, { value: 'Angel Messages', label: 'Angel Messages' }, { value: '__CREATE_NEW__', label: '+ Create New Series' } ], script_status: [ { value: 'pending', label: 'Pending' }, { value: 'draft', label: 'Draft' }, { value: 'review', label: 'Review' }, { value: 'complete', label: 'Complete' }, { value: 'approved', label: 'Approved' } ] }; const selectOptions = options[fieldName] || []; let html = ''; return html; } getCheckboxesHTML(fieldName, currentValue) { if (fieldName === 'assigned_to') { const teamMembers = [ { value: 'T', label: 'Tim - Project Manager' }, { value: 'P', label: 'Pavan - Technical Lead' }, { value: 'A', label: 'Allison - Content Creator' }, { value: 'G', label: 'Grace - Designer' }, { value: 'S', label: 'Sasha - Video Editor' } ]; const currentAssigned = Array.isArray(currentValue) ? currentValue : []; let html = '
'; teamMembers.forEach(member => { const checked = currentAssigned.includes(member.value) ? 'checked' : ''; html += ` `; }); // Add "Create New Team Member" button html += `
`; html += '
'; return html; } return ''; } getFieldLabel(fieldName) { const labels = { title: 'Video Title', youtube_description: 'YouTube Description', status: 'Status', priority: 'Priority', speakflow_link: 'SpeakFlow Link', drive_link: 'Drive Folder Link', gpt_mock_link: 'GPT Mock Link', notes: 'Notes', series: 'Series', assigned_to: 'Team Assignment' }; return labels[fieldName] || fieldName; } updateDisplayValue(element, fieldName, newValue) { if (fieldName === 'status') { element.className = element.className.replace(/status-\w+/, ''); element.classList.add(`status-${newValue}`); element.textContent = newValue.toUpperCase(); } else if (fieldName === 'priority') { const colors = { urgent: '#e53e3e', high: '#dd6b20', medium: '#38a169', low: '#718096' }; element.style.color = colors[newValue]; element.textContent = newValue.toUpperCase(); // Update row border const row = element.closest('tr'); row.className = row.className.replace(/priority-\w+/, ''); row.classList.add(`priority-${newValue}`); } else if (fieldName === 'assigned_to') { // Update team member display const teamHTML = newValue.map(initial => { const className = initial === 'T' ? 'tim' : initial === 'P' ? 'pavan' : initial === 'A' ? 'allison' : initial === 'G' ? 'grace' : initial === 'S' ? 'sasha' : ''; return `${initial}`; }).join(''); element.innerHTML = teamHTML || 'Unassigned'; } else if (fieldName.includes('link') && newValue) { element.href = newValue; element.textContent = this.getLinkDisplayText(fieldName, newValue); } else { element.textContent = newValue; } } getLinkDisplayText(fieldName, url) { const texts = { speakflow_link: 'SpeakFlow Script', drive_link: 'Drive Folder', gpt_mock_link: 'GPT Mock' }; return texts[fieldName] || 'Link'; } // Content Management showAddContentForm() { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = `

Add New Content

`; document.body.appendChild(modal); document.getElementById('newTitle').focus(); } async addNewContent() { const title = document.getElementById('newTitle').value.trim(); const youtubeTitle = document.getElementById('newYouTubeTitle').value.trim(); const series = document.getElementById('newSeries').value; const priority = document.getElementById('newPriority').value; const status = document.getElementById('newStatus').value; const description = document.getElementById('newDescription').value.trim(); const speakflow = document.getElementById('newSpeakflow').value.trim(); const drive = document.getElementById('newDrive').value.trim(); const gpt = document.getElementById('newGPT').value.trim(); const notes = document.getElementById('newNotes').value.trim(); // Get assigned team members const assignedCheckboxes = document.querySelectorAll('input[type="checkbox"]:checked'); const assigned = Array.from(assignedCheckboxes).map(cb => cb.value); if (!title) { alert('Please enter a title'); return; } try { this.showNotification('Creating content...', 'info'); const newItem = { title: title, series: series, status: status, priority: priority, youtube_title: youtubeTitle || title, youtube_description: description, speakflow_link: speakflow, drive_link: drive, gpt_mock_link: gpt, notes: notes, assigned_to: assigned }; const response = await fetch(`${this.apiUrl}/content`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newItem) }); if (!response.ok) { throw new Error('Failed to create content'); } // Reload data from API to get the new item with proper ID await this.loadData(); document.querySelector('.modal-overlay').remove(); this.showNotification(`"${title}" added successfully!`, 'success'); } catch (error) { console.error('Error creating content:', error); this.showNotification('Failed to create content', 'error'); } } // Delete Row async deleteRow(itemId) { const item = this.currentData.find(d => d.id === itemId); if (!item) return; if (confirm(`Are you sure you want to delete "${item.title}"? This cannot be undone.`)) { try { this.showNotification('Deleting...', 'info'); const response = await fetch(`${this.apiUrl}/content/${itemId}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Failed to delete content'); } // Remove from local data this.currentData = this.currentData.filter(d => d.id !== itemId); this.originalData = this.originalData.filter(d => d.id !== itemId); this.renderContent(); this.updateStats(); this.showNotification('Content deleted successfully', 'success'); } catch (error) { console.error('Error deleting content:', error); this.showNotification('Failed to delete content', 'error'); } } } // Content Rendering renderContent() { const tbody = document.querySelector('tbody'); tbody.innerHTML = ''; // Update table header for edit mode this.updateTableHeader(); this.currentData.forEach((item, index) => { const row = this.createContentRow(item, index + 1); tbody.appendChild(row); }); if (this.isEditMode) { setTimeout(() => this.setupEditableFields(), 100); } } updateTableHeader() { const thead = document.querySelector('thead tr'); const deleteHeader = thead.querySelector('.delete-header'); if (this.isEditMode && !deleteHeader) { // Add delete column header const th = document.createElement('th'); th.className = 'delete-header'; th.textContent = 'DELETE'; th.style.width = '80px'; thead.appendChild(th); } else if (!this.isEditMode && deleteHeader) { // Remove delete column header deleteHeader.remove(); } } createContentRow(item, number) { const row = document.createElement('tr'); row.setAttribute('data-id', item.id); row.className = `priority-${item.priority}`; const statusClass = `status-${item.status}`; const priorityColor = { urgent: '#e53e3e', high: '#dd6b20', medium: '#38a169', low: '#718096' }[item.priority]; const teamMembers = item.assigned_to.map(initial => { const className = initial === 'T' ? 'tim' : initial === 'P' ? 'pavan' : initial === 'A' ? 'allison' : initial === 'G' ? 'grace' : initial === 'S' ? 'sasha' : ''; return `${initial}`; }).join(''); const deleteBtn = this.isEditMode ? `` : ''; row.innerHTML = ` ${number.toString().padStart(3, '0')} ${item.status.replace('_', ' ').toUpperCase()} ${item.priority.toUpperCase()} ${item.title} ${item.series}
${this.renderThumbnailPreview(item.thumbnails || [])}
${this.getScriptStatusIcon(item.script_status)} View Description & Tags ${teamMembers} ${item.speakflow_link ? `SpeakFlow` : 'Not Started'} ${item.drive_link ? `Drive Folder` : 'Not Started'} ${item.gpt_mock_link ? `GPT Mock` : 'Not Started'} ${this.formatDate(item.upload_date)} ${item.notes} ${deleteBtn} `; return row; } getTeamMemberName(initial) { const names = { 'T': 'Tim - Project Manager', 'P': 'Pavan - Technical Lead', 'A': 'Allison - Content Creator', 'G': 'Grace - Designer', 'S': 'Sasha - Video Editor' }; return names[initial] || initial; } formatNumber(num) { if (num === 0) return '0'; if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toLocaleString(); } formatDate(dateString) { if (!dateString) return 'TBD'; const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } // Parse description and tags from combined field parseDescriptionAndTags(content) { if (!content) return { description: '', tags: [] }; const parts = content.split('#TAGS#'); if (parts.length === 1) { // No tags section, return description only return { description: content.trim(), tags: [] }; } const description = parts[0].trim(); const tagsString = parts[1].trim(); const tags = tagsString ? tagsString.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0) : []; return { description, tags }; } // Combine description and tags back into single field combineDescriptionAndTags(description, tags) { const trimmedDesc = (description || '').trim(); const validTags = Array.isArray(tags) ? tags.filter(tag => tag.trim().length > 0) : []; if (validTags.length === 0) { return trimmedDesc; } return trimmedDesc + '\n\n#TAGS#\n' + validTags.join(', '); } renderTags(tags) { if (!tags || tags.length === 0) return 'No tags'; return tags.map(tag => `${tag}`).join(''); } renderThumbnailPreview(thumbnails) { if (!thumbnails || thumbnails.length === 0) { return '
📷 Click to add
'; } const primaryThumbnail = thumbnails.find(t => t.is_primary) || thumbnails[0]; const count = thumbnails.length; return `
Thumbnail ${count > 1 ? `
+${count - 1}
` : ''}
`; } showCreateTeamMemberForm(parentModal, parentSave) { const formModal = document.createElement('div'); formModal.className = 'modal-overlay'; formModal.innerHTML = `

Add New Team Member

`; document.body.appendChild(formModal); const nameInput = formModal.querySelector('#newMemberName'); const roleInput = formModal.querySelector('#newMemberRole'); const initialInput = formModal.querySelector('#newMemberInitial'); const emailInput = formModal.querySelector('#newMemberEmail'); const createBtn = formModal.querySelector('#createMemberBtn'); nameInput.focus(); // Auto-uppercase initial initialInput.addEventListener('input', (e) => { e.target.value = e.target.value.toUpperCase(); }); createBtn.addEventListener('click', () => { const name = nameInput.value.trim(); const role = roleInput.value.trim(); const initial = initialInput.value.trim().toUpperCase(); const email = emailInput.value.trim(); if (!name || !role || !initial) { alert('Please fill in Name, Role, and Initial fields.'); return; } if (initial.length !== 1) { alert('Initial must be exactly one letter.'); return; } // Check if initial already exists const existingInitials = ['T', 'P', 'A', 'G', 'S']; // TODO: Get from API if (existingInitials.includes(initial)) { alert(`Initial "${initial}" is already used. Please choose a different letter.`); return; } // TODO: Add API call to create new team member in database this.showNotification(`New team member "${name}" (${initial}) created!`, 'success'); // Close the create form formModal.remove(); // Add new checkbox to parent form const checkbox = document.createElement('label'); checkbox.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer;'; checkbox.innerHTML = ` ${name} - ${role} `; const createButton = parentModal.querySelector('#createNewTeamMember'); createButton.parentNode.insertBefore(checkbox, createButton.previousElementSibling); }); formModal.addEventListener('click', (e) => { if (e.target === formModal) formModal.remove(); }); } showThumbnailManager(contentId) { const item = this.currentData.find(d => d.id == contentId); if (!item) return; const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = `

Manage Thumbnails - ${item.title}

📷
Drop images here or click to browse

Supports JPG, PNG, GIF (max 5MB each)

${this.renderThumbnailGrid(item.thumbnails || [])}
`; document.body.appendChild(modal); this.setupThumbnailUpload(modal, contentId); } showThumbnailViewer(contentId) { const item = this.currentData.find(d => d.id == contentId); if (!item || !item.thumbnails || item.thumbnails.length === 0) { this.showNotification('No thumbnails available', 'info'); return; } const modal = document.createElement('div'); modal.className = 'modal-overlay'; const thumbnailsHtml = item.thumbnails.map((thumb, index) => `
${thumb.original_name} ${thumb.is_primary ? '
Primary
' : ''}
${thumb.original_name}
`).join(''); modal.innerHTML = `

Thumbnails - ${item.title}

${thumbnailsHtml}
`; document.body.appendChild(modal); } renderThumbnailGrid(thumbnails) { if (!thumbnails || thumbnails.length === 0) { return '
No thumbnails yet. Upload some above!
'; } return thumbnails.map(thumb => `
${thumb.original_name}
${thumb.original_name}
${thumb.is_primary ? '
Primary
' : ''}
`).join(''); } setupThumbnailUpload(modal, contentId) { const uploadArea = modal.querySelector('#uploadArea'); const fileInput = modal.querySelector('#thumbnailFileInput'); const dropzone = modal.querySelector('.upload-dropzone'); // Click to browse dropzone.addEventListener('click', () => fileInput.click()); // File input change fileInput.addEventListener('change', (e) => { this.handleThumbnailFiles(e.target.files, contentId); }); // Drag and drop dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); }); dropzone.addEventListener('dragleave', () => { dropzone.classList.remove('drag-over'); }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.classList.remove('drag-over'); this.handleThumbnailFiles(e.dataTransfer.files, contentId); }); } async handleThumbnailFiles(files, contentId) { const validFiles = []; // Validate files first for (let file of files) { if (!file.type.startsWith('image/')) { this.showNotification(`${file.name} is not an image file`, 'error'); continue; } if (file.size > 5 * 1024 * 1024) { this.showNotification(`${file.name} is too large (max 5MB)`, 'error'); continue; } validFiles.push(file); } if (validFiles.length === 0) return; try { this.showNotification('Uploading thumbnails...', 'info'); // Create FormData for file upload const formData = new FormData(); validFiles.forEach(file => { formData.append('thumbnails', file); }); // Upload to server const response = await fetch(`${this.apiUrl}/content/${contentId}/thumbnails`, { method: 'POST', body: formData }); if (!response.ok) { throw new Error('Failed to upload thumbnails'); } const result = await response.json(); // Add uploaded thumbnails to current data const item = this.currentData.find(d => d.id == contentId); if (!item.thumbnails) item.thumbnails = []; // Add thumbnails with proper server paths result.thumbnails.forEach(thumbnail => { thumbnail.file_path = `${this.apiUrl}/uploads/${thumbnail.filename}`; item.thumbnails.push(thumbnail); }); // Update grid const grid = document.querySelector('#thumbnailsGrid'); if (grid) { grid.innerHTML = this.renderThumbnailGrid(item.thumbnails); this.setupThumbnailActions(grid, contentId); } this.showNotification(`${validFiles.length} thumbnail(s) uploaded successfully`, 'success'); } catch (error) { console.error('Error uploading thumbnails:', error); this.showNotification('Failed to upload thumbnails', 'error'); } } async setPrimaryThumbnail(thumbnailId) { // Find the thumbnail and its content let targetContent = null; let targetThumbnail = null; for (let item of this.currentData) { if (item.thumbnails) { const thumb = item.thumbnails.find(t => t.id === thumbnailId); if (thumb) { targetContent = item; targetThumbnail = thumb; break; } } } if (!targetContent || !targetThumbnail) return; // Set all thumbnails as non-primary, then set target as primary targetContent.thumbnails.forEach(t => t.is_primary = false); targetThumbnail.is_primary = true; // Update UI const grid = document.querySelector('#thumbnailsGrid'); if (grid) { grid.innerHTML = this.renderThumbnailGrid(targetContent.thumbnails); } this.showNotification('Primary thumbnail updated', 'success'); } async deleteThumbnail(thumbnailId) { if (!confirm('Are you sure you want to delete this thumbnail?')) return; // Find and remove thumbnail for (let item of this.currentData) { if (item.thumbnails) { const index = item.thumbnails.findIndex(t => t.id === thumbnailId); if (index !== -1) { item.thumbnails.splice(index, 1); // If we deleted the primary thumbnail, make the first one primary if (item.thumbnails.length > 0 && !item.thumbnails.some(t => t.is_primary)) { item.thumbnails[0].is_primary = true; } // Update UI const grid = document.querySelector('#thumbnailsGrid'); if (grid) { grid.innerHTML = this.renderThumbnailGrid(item.thumbnails); } this.showNotification('Thumbnail deleted', 'success'); return; } } } } getScriptStatusIcon(status) { const icons = { pending: '❌ Pending', draft: '📝 Draft', complete: '✅ Complete', review: '🔍 Review' }; return icons[status] || status; } // Filtering and Search filterContent(filterValue) { const rows = document.querySelectorAll('tbody tr'); const filter = filterValue.toLowerCase(); rows.forEach(row => { if (filter === 'all content' || filter === '') { row.style.display = ''; } else { const status = row.querySelector('[data-field="status"]').textContent.toLowerCase(); const series = row.querySelector('[data-field="series"]').textContent.toLowerCase(); if (status.includes(filter) || series.includes(filter)) { row.style.display = ''; } else { row.style.display = 'none'; } } }); } // Reports and Analytics generateReport() { const total = this.currentData.length; const statusCounts = this.getStatusCounts(); const seriesCounts = this.getSeriesCounts(); const reportHTML = `

📊 Content Performance Report

Generated: ${new Date().toLocaleDateString()}

📈 Status Overview
${Object.entries(statusCounts).map(([status, count]) => `
• ${status}: ${count} videos (${Math.round(count/total*100)}%)
` ).join('')}
📺 Series Breakdown
${Object.entries(seriesCounts).map(([series, count]) => `
• ${series}: ${count} videos (${Math.round(count/total*100)}%)
` ).join('')}
⚡ Priority Distribution
${this.getPriorityBreakdown().map(p => `
• ${p.priority}: ${p.count} videos
` ).join('')}
💡 Recommendations:
`; this.showModal('Content Performance Report', reportHTML); } getStatusCounts() { return this.currentData.reduce((acc, item) => { acc[item.status] = (acc[item.status] || 0) + 1; return acc; }, {}); } getSeriesCounts() { return this.currentData.reduce((acc, item) => { acc[item.series] = (acc[item.series] || 0) + 1; return acc; }, {}); } getPriorityBreakdown() { const counts = this.currentData.reduce((acc, item) => { acc[item.priority] = (acc[item.priority] || 0) + 1; return acc; }, {}); return Object.entries(counts).map(([priority, count]) => ({ priority: priority.charAt(0).toUpperCase() + priority.slice(1), count })); } // Statistics Update updateStats() { const total = this.currentData.length; const uploaded = this.currentData.filter(d => d.status === 'uploaded').length; const inProgress = this.currentData.filter(d => ['script', 'recording', 'editing'].includes(d.status)).length; const pending = this.currentData.filter(d => d.status === 'pending').length; document.querySelectorAll('.stat-number')[0].textContent = total; document.querySelectorAll('.stat-number')[1].textContent = uploaded; document.querySelectorAll('.stat-number')[2].textContent = inProgress; document.querySelectorAll('.stat-number')[3].textContent = pending; } // Utility Methods showModal(title, content) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = `

${title}

${content}
`; document.body.appendChild(modal); } showNotification(message, type = 'info') { const colors = { success: '#10b981', error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' }; const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${colors[type]}; color: white; padding: 12px 20px; border-radius: 6px; z-index: 2000; font-weight: 500; box-shadow: 0 10px 20px rgba(0,0,0,0.1); max-width: 400px; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { if (notification.parentNode) { notification.style.opacity = '0'; notification.style.transform = 'translateX(100%)'; setTimeout(() => notification.remove(), 300); } }, 4000); } } // Initialize CMS when DOM is loaded let cms; document.addEventListener('DOMContentLoaded', () => { cms = new SoulfulCMS(); }); // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = SoulfulCMS; }